本教程演示了如何使用Django表单集和JavaScript将表单的多个副本动态添加到页面并进行处理。
在Web应用程序中,如果用户正在输入数据以将对象添加到数据库中,则用户可能需要连续多次提交相同的表单。 Django不必一遍又一遍地提交相同的表单,而是允许我们使用表单集将相同表单的多个副本添加到网页。
我们将通过观鸟应用程序演示此操作,以跟踪用户看到的鸟类。 它仅包括两页,一个包含鸟类列表的页面和一个将鸟类添加到列表的表单的页面。 您可以在GitHub上查看此示例的完整源代码。
对于本教程,对以下内容有一个基本的了解会有所帮助:
Django表格
基于Django类的视图
JavaScript DOM操作
建立
模型
该应用程序只有一个模型Bird,该模型存储有关我们见过的每只鸟的信息。
1 2 3 4 5 6 7 8 9 |
# models.py from django.db import models class Bird(models.Model): common_name = models.CharField(max_length=250) scientific_name = models.CharField(max_length=250) def __str__(self): return self.common_name |
模型由两个CharField组成,分别代表鸟类的通用名称和科学名称
网址
我们将使用项目级别urls.py文件设置为包含应用程序级别urls.py文件的URL进行管理。
1 2 3 4 5 6 7 8 |
# project level urls.py from django.contrib import admin from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('', include('birds.urls')), ] |
应用程序级别urls.py将包含应用程序两个页面的路径。
1 2 3 4 5 6 7 8 |
# app level urls.py from django.urls import path from .views import BirdAddView, BirdListView urlpatterns = [ path('add', BirdAddView.as_view(), name="add_bird"), path('', BirdListView.as_view(), name="bird_list") ] |
Views
最初,我们将仅为列出鸟类的页面设置视图。 该视图将使用通用ListView获取所有鸟类实例,并使它们可显示在指定模板中。
1 2 3 4 5 6 7 |
# views.py from django.views.generic import ListView from .models import Bird class BirdListView(ListView): model = Bird template_name = "bird_list.html" |
我们稍后将添加表单页面的视图。
Formsets
此时,我们可以创建一个表单和一个将表单提供给模板的视图,以便用户可以向列表中添加新鸟。 但是用户一次只能添加一只鸟。 要一次添加多只鸟,我们需要使用表单集而不是常规表单。
表单集使我们可以在单个页面上拥有同一表单的多个副本。 如果用户应该能够一次提交多个表单以创建多个模型实例,这将很有帮助。 在我们的示例中,这将使用户能够一次将多只鸟添加到其列表中,而不必为每只鸟提交表单。
模型表单集
可以为不同类型的表单(包括模型表单)创建表单集。 由于我们要创建Bird模型的新实例,因此模型模板很自然。
如果我们只想向页面添加单个模型表单,则可以这样创建:
1 2 3 4 5 6 7 8 9 |
# forms.py from django.forms import ModelForm from .models import Bird # A regular form, not a formset class BirdForm(ModelForm): class Meta: model = Bird fields = [common_name, scientific_name] |
模型表单要求我们指定要与表单关联的模型以及要包含在表单中的字段。
要创建模型表单集,我们根本不需要定义模型表单。 取而代之的是,我们使用Django的modelformset_factory()来返回给定模型的formset类。
1 2 3 4 5 6 7 |
# forms.py from django.forms import modelformset_factory from .models import Bird BirdFormSet = modelformset_factory( Bird, fields=("common_name", "scientific_name"), extra=1 ) |
modelformset_factory()要求第一个参数是表单集适用的模型。指定模型后,可以指定其他可选参数。我们将使用两个可选参数-字段和额外。
字段-指定要在表单中显示的字段
extra-最初在页面上显示的表单数
通过将extra设置为1,我们最初将仅将一种形式传递给模板。一个是默认值,但是为了清楚起见,我们将明确指定它。将extra设置为任何其他数字将为模板提供该数量的表单。
Extra允许我们向用户显示表单的多个副本,但是我们可能不知道用户一次要添加多少只鸟。如果我们显示的表格太少,那么用户仍将不得不多次提交该表格。如果我们显示太多表单,则用户可能必须向下滚动页面以查找提交按钮。幸运的是,通过设置额外的内容,我们不仅限于拥有这么多表格副本。即使将extra设置为1,我们也可以向页面中添加实际需要的表单副本数(最多1000个副本)。我们可以使用JavaScript动态将表单的其他副本添加到页面中。我们将在创建用于显示表单的模板时执行此操作。在此之前,我们需要创建视图。
视图
我们需要一个可以处理GET请求以初始显示表单的视图,而提交表单时则需要POST请求。我们将基于类的TemplateView用于基本视图功能,并为GET和POST请求添加方法。
GET请求要求我们创建表单集的实例并将其添加到上下文中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
# views.py from django.views.generic import ListView, TemplateView # Import TemplateView from .models import Bird from .forms import BirdFormSet # Import the formset class BirdListView(ListView): model = Bird template_name = "bird_list.html" # View for adding birds class BirdAddView(TemplateView): template_name = "add_bird.html" # Define method to handle GET request def get(self, *args, **kwargs): # Create an instance of the formset formset = BirdFormSet(queryset=Bird.objects.none()) return self.render_to_response({'bird_formset': formset}) |
要创建表单集实例,我们首先导入表单集,然后在get方法中对其进行调用。 如果仅调用不带参数的表单集,则将获得一个表单集,其中包含数据库中所有Bird实例的表单。 由于我们希望该视图仅添加新的鸟类,因此我们需要防止所显示的表单预先填充有Bird实例。 为此,我们指定了一个自定义查询集。 我们将queryset参数设置为Bird.objects.none(),这将创建一个空的queryset。 这样,将不会在表格中预先填充任何鸟类。
创建表单集实例后,我们将调用render_to_response,其参数是一个字典,该字典具有分配给bird_formset键的表单集。
对于POST请求,我们需要定义一个post方法,用于在提交表单时处理该表单。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
# views.py from django.views.generic import ListView, TemplateView from .models import Bird from .forms import BirdFormSet from django.urls import reverse_lazy from django.shortcuts import redirect class BirdListView(ListView): model = Bird template_name = "bird_list.html" class BirdAddView(TemplateView): template_name = "add_bird.html" def get(self, *args, **kwargs): formset = BirdFormSet(queryset=Bird.objects.none()) return self.render_to_response({'bird_formset': formset}) # Define method to handle POST request def post(self, *args, **kwargs): formset = BirdFormSet(data=self.request.POST) # Check if submitted forms are valid if formset.is_valid(): formset.save() return redirect(reverse_lazy("bird_list")) return self.render_to_response({'bird_formset': formset}) |
首先,我们创建BirdFormSet的实例。 这次,我们通过self.request.POST包含了来自请求的提交数据。 创建表单集后,我们必须对其进行验证。 与常规表单类似,可以使用表单集调用is_valid()来验证提交的表单和表单字段。 如果该表单集有效,则我们在该表单集上调用save(),它将创建新的Bird对象并将其添加到数据库中。 完成此操作后,我们会将用户重定向到包含鸟类列表的页面。 如果表单集无效,则会将表单集返回给用户,并附带相应的错误消息。
模板
现在我们已经设置了视图,我们可以创建将呈现给用户的模板。
为了显示鸟类列表,我们将遍历ListView提供的object_list,并包括数据库中的所有鸟类实例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Bird List</title> </head> <body> <h1>Bird List</h1> <a href='{% url "add_bird" %}'>Add bird</a> {% for bird in object_list %} <p>{{bird.common_name}}: {{bird.scientific_name}}</p> {% endfor %} </body> </html> |
我们将为每只鸟显示通用名称和科学名称。 并包含指向该页面的链接,其中包含用于向列表中添加鸟的表格。
我们添加小鸟页面的模板最初将只显示我们指定的带有额外样式的表单。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Add bird</title> </head> <body> <h1>Add a new bird</h1> <form id="form-container" method="POST"> {% csrf_token %} {{bird_formset.management_form}} {% for form in bird_formset %} <div class="bird-form"> {{form.as_p}} </div> {% endfor %} <button type="submit">Create Birds</button> </form> </body> </html> |
我们创建一个ID为form-container和POST方法的HTML <form>。 像常规形式一样,我们包含csrf_token。 与常规表单不同,我们必须包含{{bird_formset.management_form}}。 这会将管理表单插入页面,该页面包含隐藏的输入,这些隐藏的输入包含有关正在显示的表单数量的信息,并在提交表单时由Django使用。
1 2 3 4 |
<input type="hidden" name="form-TOTAL_FORMS" value="1" id="id_form-TOTAL_FORMS"> <input type="hidden" name="form-INITIAL_FORMS" value="0" id="id_form-INITIAL_FORMS"> <input type="hidden" name="form-MIN_NUM_FORMS" value="0" id="id_form-MIN_NUM_FORMS"> <input type="hidden" name="form-MAX_NUM_FORMS" value="1000" id="id_form-MAX_NUM_FORMS"> |
form-TOTAL_FORMS输入包含正在提交的表单总数的值。 如果与提交表单时Django收到的实际表单数不匹配,则会引发错误。
使用for循环将表单集中的表单添加到页面中,以遍历表单集中的每个表单并将其呈现。 我们选择使用{{form.as_p}}在<p>标记中显示表单。 表单以HTML形式呈现为:
1 2 3 4 5 6 7 8 9 |
<p> <label for="id_form-0-common_name">Common name:</label> <input type="text" name="form-0-common_name" maxlength="250" id="id_form-0-common_name"> </p> <p> <label for="id_form-0-scientific_name">Scientific name:</label> <input type="text" name="form-0-scientific_name" maxlength="250" id="id_form-0-scientific_name"> <input type="hidden" name="form-0-id" id="id_form-0-id"> </p> |
表单中的每个字段都有一个name和id属性,其中包含一个数字和该字段的名称。 通用名称字段的名称属性为form-0-common_name,id为id_form-0-common_name。 这些很重要,因为这两个属性中的数字都用于标识字段所属的表单。 对于我们的单个表单,每个字段都是表单0的一部分,因为表单编号从0开始。
尽管此模板可以工作并允许用户一次提交一只鸟,但是如果用户想要添加多只鸟,它仍然不允许我们添加更多表格。 我们可以通过将extra设置为其他数字来添加更多表单。 取而代之的是,我们将使用JavaScript允许用户选择他们要提交的表格数量。
使表单集动态化
由于我们使用的是表单集,并且已设置视图来处理表单集而不是常规表单,因此用户可以一次提交任意数量的表单。 我们只需要在页面上提供这些表格即可。 可以使用JavaScript对DOM进行操作。
添加其他表单需要使用JavaScript来:
从页面获取现有表单
复印表格
递增表格数
在页面上插入新表格
更新管理表格中的表格总数
在使用JavaScript之前,我们将向HTML表单添加另一个按钮,该按钮将用于向页面添加其他表单。
1 |
<button id="add-form" type="button">Add Another Bird</button> |
该按钮将直接添加在“创建鸟类”按钮之前。
要获取页面上的现有表单,我们使用document.querySelectorAll(’。bird-form’),其中bird-form是分配给div的类,该div包含每个表单的字段。 虽然document.querySelectorAll(’。bird-form’)将返回页面上所有具有一类鸟形形式的元素的列表,但此时我们在页面上仅具有一个形式,因此它将返回一个列表 仅包含一种形式。 我们将使用document.querySelector()获取页面上执行所有步骤所需的其他元素,即整个HTML表单,用户可以单击以向页面添加新表单的按钮以及表单的总输入 管理表格。
1 2 3 4 |
let birdForm = document.querySelectorAll(".bird-form") let container = document.querySelector("#form-container") let addButton = document.querySelector("#add-form") let totalForms = document.querySelector("#id_form-TOTAL_FORMS") |
我们还需要获取页面上最后一个表格的编号。 同样,由于我们知道最初仅一个表单会显示在页面上,所以我们可以说最后一个表单是表单0。但是如果我们最初希望在页面上显示多个表单,则可以通过使用 储存在birdForm中的表格数量,并减去1以说明从0开始的表格编号。
1 |
let formNum = birdForm.length-1 // Get the number of the last form on the page with zero-based indexing |
我们只希望在用户单击“添加其他鸟”按钮时将新表单添加到页面。 这意味着其余步骤仅应在按下按钮时执行。 我们需要创建一个执行其余步骤的功能,然后使用事件监听器将其附加到按钮按下。 我们将调用函数addForm,以便可以将其关联到click by按钮。
1 |
addButton.addEventListener('click', addForm) |
addForm函数将如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 |
function addForm(e) { e.preventDefault() let newForm = birdForm[0].cloneNode(true) //Clone the bird form let formRegex = RegExp(`form-(\d){1}-`,'g') //Regex to find all instances of the form number formNum++ //Increment the form number newForm.innerHTML = newForm.innerHTML.replace(formRegex, `form-${formNum}-`) //Update the new form to have the correct form number container.insertBefore(newForm, addButton) //Insert the new form at the end of the list of forms totalForms.setAttribute('value', `${formNum+1}`) //Increment the number of total forms in the management form } |
首先,我们阻止按钮单击的默认操作,因此仅执行我们的addForm函数。然后,通过使用.cloneNode()克隆birdForm来创建新表单。我们将true作为参数传递,因此birdForm的所有子节点也都被复制到newForm中。
由于创建了副本,因此newForm包含与页面上已经存在的表单完全相同的属性值。这意味着表格编号是相同的。由于我们无法提交两个具有相同编号的表格,因此我们需要增加表格的编号。为此,我们增加formNum,然后使用正则表达式在newForm的HTML中查找并替换表单编号的所有实例。
通过查看表单的HTML,我们可以看到所有包含表单编号的属性都包含一个常见的格式为-0-。我们将创建一个正则表达式以匹配此模式并将其保存到formRegex。
接下来,我们将在newForm的HTML中标识与formRegex匹配的所有实例,并将其替换为新的字符串。我们希望替换字符串与匹配的模式相同,期望该数字现在是最近增加的formNum的值。通过使用.innerHTML访问newForm的HTML,然后使用.replace(),我们可以匹配正则表达式并执行替换。
现在我们在newForm中具有正确的表单号,我们可以将其添加到页面中。我们将使用.insertBefore()并将新表单添加到addButton之前。
现在有了页面上的表单,我们只需要确保已使用将要提交的正确数量的表单正确地更新了管理表单。我们需要增加form-TOTAL_FORMS隐藏字段的值。我们已经将该字段保存在totalForms中。要对其进行更新,我们将使用.setAttribute()将value属性设置为比当前表单编号大一的值。我们做得更多,因为form-TOTAL_FORMS包含页面上的表单数量,从1开始,而单个表单编号从0开始。
现在,我们可以将所有JavaScript添加到<script>标记中,然后关闭<body>标记,以获取最终模板。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Add bird</title> </head> <body> <h1>Add a new bird</h1> <form id="form-container" method="POST"> {% csrf_token %} {{bird_formset.management_form}} {% for form in bird_formset %} <div class="bird-form"> {{form.as_p}} </div> {% endfor %} <button id="add-form" type="button">Add Another Bird</button> <button type="submit">Create Birds</button> </form> <script> let birdForm = document.querySelectorAll(".bird-form") let container = document.querySelector("#form-container") let addButton = document.querySelector("#add-form") let totalForms = document.querySelector("#id_form-TOTAL_FORMS") let formNum = birdForm.length-1 addButton.addEventListener('click', addForm) function addForm(e){ e.preventDefault() let newForm = birdForm[0].cloneNode(true) let formRegex = RegExp(`form-(\d){1}-`,'g') formNum++ newForm.innerHTML = newForm.innerHTML.replace(formRegex, `form-${formNum}-`) container.insertBefore(newForm, addButton) totalForms.setAttribute('value', `${formNum+1}`) } </script> </body> </html> |
而已! 现在,用户可以通过单击“添加另一只鸟”按钮向页面添加任意数量的表单,并且在提交表单后将其保存到数据库中,该用户将被重定向回鸟列表,并且 将显示所有新提交的鸟。
摘要
Django表单集允许我们在单个页面上包含同一表单的多个副本,并在提交时正确处理它们。 此外,我们可以让用户通过使用JavaScript向页面添加更多表单来选择要提交的表单数量。 尽管本教程使用了modelformsets,但也可以使用其他类型的表单的表单集。
原文:https://www.brennantymrak.com/articles/django-dynamic-formsets-javascript
GIT地址:https://github.com/bmtymrak/dynamic-formset-demo